using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using AzureContrib.WindowsAzure.Properties;
using AzureContrib.WindowsAzure.ServiceRuntime;
using AzureContrib.WindowsAzure.Utils;
using Microsoft.WindowsAzure.ServiceRuntime;
using RoleEnvironment = AzureContrib.WindowsAzure.ServiceRuntime.RoleEnvironment;
using SD = System.Diagnostics;

namespace AzureContrib.WindowsAzure.Diagnostics
{
	/// <summary>
	/// This <see cref="ITraceSwitchProvider"/> produces <see cref="SD.TraceSwitch"/> instances based on configuration read from the Windows Azure Config.
	///
	/// The configuration setting names are found using <see cref="TraceSwitchKeyBase"/>, <see cref="DefaultTraceSwitchKey"/> and <see cref="TraceSwitchKey"/>.
	///
	/// The format of the value for a trace switch setting is 'switch:{switch}|tracelevel:{tracelevel}[|description:{description}]' or for short 's:{switch}|t:{tracelevel}[|d:{description}]'.
	/// </summary>
	public class WindowsAzureTraceSwitchProvider : ITraceSwitchProvider
	{
		private SD.TraceSwitch defaultTraceSwitch;
		private SD.TraceLevel defaultTraceLevel;
		private IDictionary<string, SD.TraceSwitch> traceSwitches;

		/// <summary>
		/// The separation character for parts of trace switch data.
		/// </summary>
		public const char TraceSwitchWildCard = '*';

		/// <summary>
		/// The separation character for parts of trace switch data.
		/// </summary>
		public const char TraceDataSeparator = '|';

		/// <summary>
		/// The separation character between a key/value trace switch data pair
		/// </summary>
		public const char KeyValueSeparator = ':';

		/// <summary>
		/// The base for a trace switch setting key in Windows Azure Config.
		/// </summary>
		public static readonly string TraceSwitchKeyBase = string.Concat(typeof(WindowsAzureTraceSwitchProvider).FullName, ".Setting.");

		/// <summary>
		/// The Windows Azure configuration key for the default trace switch.
		/// </summary>
		public static readonly string DefaultTraceSwitchKey = string.Concat(TraceSwitchKeyBase, "Default");

		private IDictionary<string, SD.TraceSwitch> configuredPatterns;
		internal IRoleEnvironment RoleEnvironment;

		/// <summary>
		/// Use this method to generate the next trace switch setting key for use in Windows Azure config.
		/// </summary>
		/// <param name="index">The index of the key to generate</param>
		/// <returns>A key to use in Windows Azure config for diagnostic trace switch data.</returns>
		public static string TraceSwitchKey(int index)
		{
			if (index < 0) throw new ArgumentOutOfRangeException("index", index, Strings.HasToBeZeroOrMore);

			return string.Concat(TraceSwitchKeyBase, index.ToString());
		}

		/// <summary>
		/// Create an instance of the provider of <see cref="SD.TraceSwitch"/> for Windows Azure.
		/// </summary>
		/// <param name="defaultTraceLevel">The default trace level to use if no config is present.</param>
		public WindowsAzureTraceSwitchProvider(SD.TraceLevel defaultTraceLevel = SD.TraceLevel.Warning)
		{
			ResetTraceSwitches(new RoleEnvironment.WrappedRoleEnvironment(), defaultTraceLevel);
		}

		/// <summary>
		/// Create an instance of the provider of <see cref="SD.TraceSwitch"/> for Windows Azure.
		/// </summary>
		/// <param name="roleEnvironment"><see cref="IRoleEnvironment"/></param>
		/// <param name="defaultTraceLevel">The default trace level to use if no config is present.</param>
		public WindowsAzureTraceSwitchProvider(IRoleEnvironment roleEnvironment, SD.TraceLevel defaultTraceLevel = SD.TraceLevel.Warning)
		{
			ResetTraceSwitches(roleEnvironment, defaultTraceLevel);
		}

		internal void ResetTraceSwitches(IRoleEnvironment roleEnvironment, SD.TraceLevel defaultTraceLevel)
		{
			if (RoleEnvironment == null)
			{
				RoleEnvironment = roleEnvironment ?? new RoleEnvironment.WrappedRoleEnvironment();
				RoleEnvironment.Changing += RoleEnvironmentChangingHandler;
			}
			this.defaultTraceLevel = defaultTraceLevel;

			configuredPatterns = new Dictionary<string, SD.TraceSwitch>();
			traceSwitches = new Dictionary<string, SD.TraceSwitch>();

			// Temp default trace switch
			defaultTraceSwitch = new SD.TraceSwitch(TraceSwitchWildCard.ToString(), "Temporary default trace switch.", SD.TraceLevel.Verbose.ToString());

			// Get the configured trace switches
			var realDefaultTraceSwitch = GetDefaultTraceSwitch(defaultTraceLevel);
			var switches = GetConfiguredTraceSwitches();

			// Set the configured trace switches
			defaultTraceSwitch = new SD.TraceSwitch(TraceSwitchWildCard.ToString(), realDefaultTraceSwitch.Description, realDefaultTraceSwitch.Level.ToString());
			configuredPatterns.Clear();
			configuredPatterns.Add(CleanRegexPattern(defaultTraceSwitch.DisplayName), defaultTraceSwitch);
			traceSwitches.Clear();
			foreach (var traceSwitch in switches)
			{
				configuredPatterns.Add(CleanRegexPattern(traceSwitch.DisplayName), traceSwitch);
				traceSwitches.Add(traceSwitch.DisplayName, traceSwitch);
			}
		}

		internal IEnumerable<SD.TraceSwitch> GetConfiguredTraceSwitches()
		{
			var switches = new List<SD.TraceSwitch>();
			string traceSwitchData;
			int i = 0;
			while (RoleEnvironment.TryGetConfigurationSettingValue(TraceSwitchKey(i++), out traceSwitchData))
			{
				SD.Trace.WriteLine("Found a trace setting in Windows Azure config with the value: '{0}'".F(traceSwitchData), SD.TraceLevel.Verbose.ToString());

				var traceSetting = GetTraceSwitch(traceSwitchData);
				switches.Add(traceSetting);
			}

			SD.Trace.WriteLine("Found a total of {0} trace settings in Windows Azure config.".F(switches.Count), SD.TraceLevel.Verbose.ToString());

			return switches;
		}

		internal SD.TraceSwitch GetDefaultTraceSwitch(SD.TraceLevel defaultTraceLevel)
		{
			string defaultTraceSwitchData;
			SD.TraceSwitch traceSwitch;
			if (RoleEnvironment.TryGetConfigurationSettingValue(DefaultTraceSwitchKey, out defaultTraceSwitchData))
			{
				SD.Trace.WriteLine("Found a default trace setting in Windows Azure config with the value: '{0}'".F(defaultTraceSwitchData), SD.TraceLevel.Verbose.ToString());

				if (defaultTraceSwitchData.Split(TraceDataSeparator).Length == 1)
				{
					defaultTraceSwitchData = string.Format("{0}|{1}", TraceSwitchWildCard, defaultTraceSwitchData);
				}

				traceSwitch = GetTraceSwitch(defaultTraceSwitchData);
			}
			else
			{
				SD.Trace.WriteLine("Did not find a default trace setting '{0}' in Windows Azure config. Using default trace level {1} all over.".F(DefaultTraceSwitchKey, defaultTraceLevel), SD.TraceLevel.Verbose.ToString());
				traceSwitch = new SD.TraceSwitch(TraceSwitchWildCard.ToString(), null)
																{
																	Level = defaultTraceLevel
																};
			}
			return traceSwitch;
		}

		internal static SD.TraceSwitch GetTraceSwitch(string traceSwitchData)
		{
			if (string.IsNullOrEmpty(traceSwitchData))
				throw new ArgumentNullException("traceSwitchData", Strings.TraceSwitchDataCannotBeNullOrEmpty);

			var traceSwitchDataParts = traceSwitchData.Split(TraceDataSeparator);

			if (traceSwitchDataParts.Length < 2 || traceSwitchDataParts.Length > 3)
			{
				throw new ArgumentException(string.Format("Unable to parse the configured switch data: <{0}>", traceSwitchData));
			}

			string displayName = null;
			string description = null;
			string defaultSwitchValue = null;

			var i = 0;
			var propertyNames = new[] { "switch", "tracelevel", "description" };
			foreach (var kvp in traceSwitchDataParts.Select(traceSwitchDataPart => traceSwitchDataPart.Split(KeyValueSeparator)))
			{
				var keyValue = kvp;

				if (keyValue.Length == 1)
				{
					keyValue = new[] { propertyNames[i], keyValue[0] };
				}

				switch (keyValue[0].ToLower())
				{
					case "switch":
					case "s":
						displayName = keyValue[1];
						break;
					case "tracelevel":
					case "t":
						defaultSwitchValue = keyValue[1];
						break;
					case "description":
					case "d":
						description = keyValue[1];
						break;
					default:
						throw new ArgumentException("The data read from config has to have the format 'switch:{switch}|tracelevel:{tracelevel}[|description:{description}]'");
				}

				i++;
			}

			if (defaultSwitchValue == "Information") defaultSwitchValue = SD.TraceLevel.Info.ToString();

			if (string.IsNullOrEmpty(displayName)) throw new ArgumentException("The data read from config does not contain the data for 'switch'.");
			if (string.IsNullOrEmpty(defaultSwitchValue)) throw new ArgumentException("The data read from config does not contain the data for 'tracelevel'.");

			return new SD.TraceSwitch(displayName, description, defaultSwitchValue);
		}

		internal void RoleEnvironmentChangingHandler(object sender, RoleEnvironmentChangingEventArgs e)
		{
			if (!e.Changes
						.Where(rec => rec is RoleEnvironmentConfigurationSettingChange)
						.Cast<RoleEnvironmentConfigurationSettingChange>()
						.Any(recsc => recsc.ConfigurationSettingName.StartsWith(TraceSwitchKeyBase)))
			{
				return;
			}

			ResetTraceSwitches(null, defaultTraceLevel);
		}

		/// <summary>
		/// Unhooks the <see cref="IRoleEnvironment"/> changing event from this instance.
		/// </summary>
		public void Dispose()
		{
			RoleEnvironment.Changing -= RoleEnvironmentChangingHandler;
		}

		/// <summary>
		/// Find the <see cref="SD.TraceSwitch"/> for this <see cref="ITraceSwitchable"/>?
		/// </summary>
		/// <returns><see cref="SD.TraceSwitch"/></returns>
		public SD.TraceSwitch GetSwitch(ITraceSwitchable traceSwitchable)
		{
			SD.TraceSwitch value;
			if (traceSwitches.TryGetValue(traceSwitchable.Switch, out value))
			{
				return value;
			}

			var mostVerboseMatch = configuredPatterns.Where(kvp => Regex.IsMatch(traceSwitchable.Switch, kvp.Key))
																							 .Select(kvp => kvp.Value)
																							 .OrderByDescending(ts => ts.Level).FirstOrDefault();
			if (mostVerboseMatch == null)
			{
				mostVerboseMatch = defaultTraceSwitch;
			}

			traceSwitches.Add(traceSwitchable.Switch, mostVerboseMatch);

			return mostVerboseMatch;
		}

		internal static string CleanRegexPattern(string pattern)
		{
			if (string.IsNullOrEmpty(pattern))
			{
				return pattern;
			}

			// allocate a StringBuilder with some overhead compared to incoming
			// string, which should allow for the regex to expand a bit without
			// need for further allocations
			//var sb = new StringBuilder((int)(pattern.Length * 1.2));
			var sb = new StringBuilder((int)(pattern.Length * 1.5) + 3);

			foreach (var character in pattern)
			{
				switch (character)
				{
					case '.':
						{
							sb.Append(@"\.");
							break;
						}
					case '*':
						{
							sb.Append(".*?");
							break;
						}
					default:
						{
							sb.Append(character);
							break;
						}
				}
			}
			return sb.ToString();

			// implementation with less code, but slower (especially in Release builds)
			// return incomingSwitch
			//    .Replace(".", @"\.")
			//    .Replace("*", ".*?");
		}
	}
}